<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./assets/global.css">

    <style>
        .game-container {
            width: 100%;
            height: 100vh;
            position: relative;
            overflow: hidden;
        }

        .tile-container {
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            display: flex;
            align-items: center;
            justify-content: center;
            background-size: cover;
            background-position: center;
        }

        .tile-container .tile-item {
            position: absolute;
            cursor: default;
            user-select: none;
            display: flex;
            align-items: center;
            justify-content: center;
            background-repeat: no-repeat;
            background-size: cover;
            background-position: center;
        }

        .tile-container .tile-item.active {
            box-sizing: border-box;
            border: 2px solid skyblue;
        }

        #drawLine,
        #win {
            position: absolute;
            width: 100%;
            height: 100%;
            z-index: 2;
            pointer-events: none;
        }

        #win {
            background-size: cover;
        }

        .sound-container {
            position: absolute;
            opacity: 0;
            pointer-events: none;
        }
    </style>
</head>

<body>
    <div class="game-container">
        <div class="tile-container">
            <canvas id="drawLine">
                抱歉，您的浏览器不支持 canvas 元素
            </canvas>
            <div id="win"></div>
        </div>
    </div>
    <div class="sound-container">
        <audio id="sound_audio"></audio>
    </div>
    <script type="module">
        import { Randoms } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/randoms.js";
        // import { cloneDeep } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/common.js";
        import { aspectFill, aspectFit, scaleToFill } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/picture.js";

        let configPath = './assets/lianliankan/config.json'
        // 配置信息
        let config = {
            cols: 0,
            rows: 0,
            width: 400,
            height: 400,
            capacity: 0, // 容量 
            get itemWidth() {
                return this.width / this.rows
            },
            get itemHeight() {
                return this.height / this.cols;
            },
            get canvasWidth() {
                return this.width + 2 * this.itemWidth
            },
            get canvasHeight() {
                return this.height + 2 * this.itemHeight
            }
        }
        // 游戏容器
        let gameContainerDom = document.querySelector('.game-container')
        // 获取格子容器
        let tileContainerDom = document.querySelector('.tile-container')
        // 画线元素
        /** @type {HTMLCanvasElement} */
        let drawLineDom = document.querySelector('#drawLine')
        // 胜利元素
        /** @type {HTMLDivElement} */
        let winDom = document.querySelector('#win')
        // 遮罩数据
        let maskData = []
        // 第一个元素
        /** @type {HTMLDivElement} */
        let firstDom = null
        // 第二个元素
        /** @type {HTMLDivElement} */
        let lastDom = null
        // 地图数据
        let mapData = {}
        // 加载配置
        function loadConfig() {
            fetch(configPath).then((response) => {
                return response.text()
            }).then((response) => {
                Object.assign(config, JSON.parse(response))
                initMask();
            })
        }
        // 初始化遮罩
        function initMask() {
            // 读取遮罩地图 
            let maskImg = new Image();
            maskImg.src = config.mask
            maskImg.onload = (ev) => {
                let cv = document.createElement('canvas');
                let ctx = cv.getContext('2d')
                cv.width = maskImg.width;
                cv.height = maskImg.height;
                ctx.drawImage(maskImg, 0, 0)
                // 获取遮罩数据
                let colorData = ctx.getImageData(0, 0, cv.width, cv.height)
                let colorArrData = group(colorData.data, (item, index) => index % 4)

                maskData = group(colorArrData.shift().map(item => item == 255 ? 1 : 0), (item, index) => index % cv.width);
                config.rows = maskImg.width;
                config.cols = maskImg.height;
                config.capacity = maskData.flat().filter(item => item).length
                let maskDataPreview = '预览遮罩数据\n';
                for (let x = 0; x < config.rows; x++) {
                    for (let y = 0; y < config.cols; y++) {
                        // console.log(x, y);
                        maskDataPreview += `${maskData[x][y]} `
                    }
                    maskDataPreview += '\n'
                }
                console.log(maskDataPreview);
                // 执行初始化配置
                initConfig();
            }
        }
        // 初始化配置
        function initConfig() {
            console.log("读取配置", config);
            // 读取配置文件 

            // 自适应
            if (config.adaptation) {
                console.log(config.width, config.height);
                let ow = config.width;
                let oh = config.height;
                let tw = gameContainerDom.clientWidth;
                let th = gameContainerDom.clientHeight;
                console.log(tw, th);
                let pictureParams = [ow, oh, tw, th]
                let pictureFunc = {
                    aspectFill, aspectFit, scaleToFill
                }
                let pictureRes = pictureFunc[config.adaptation](...pictureParams)
                config.width = pictureRes.width;
                config.height = pictureRes.height;
                console.log(config.width, config.height);
            }
            tileContainerDom.setAttribute("style", `width:${config.width}px;height:${config.height}px`);
            tileContainerDom.style.backgroundImage = `url(${config.background})`
            winDom.style.backgroundImage = `url(${config.win})`
            winDom.hidden = true
            // 初始化地图
            for (let x = 0; x < config.rows; x++) {
                for (let y = 0; y < config.cols; y++) {
                    if (maskData[x][y]) {
                        console.log(config.itemWidth, config.itemHeight);
                        let tileDom = document.createElement('div');
                        tileDom.classList.add('tile-item')
                        tileDom.setAttribute("style", `width:${config.itemWidth}px;height:${config.itemHeight}px;left:${x * config.itemWidth}px;top:${y * config.itemHeight}px`)
                        tileDom.setAttribute('x', x)
                        tileDom.setAttribute('y', y)
                        // tileDom.textContent = maskData[x][y]
                        tileDom.addEventListener("click", tileClick)
                        tileContainerDom.appendChild(tileDom);
                    }
                }
            }
            // 初始化画布
            drawLineDom.setAttribute("style", `width:${config.canvasWidth}px;height:${config.canvasHeight}px`)
            initTile();
        }
        // 初始化砖块
        function initTile() {
            let tiles = config.tiles
            let tileList = []
            let minLoopCount = config.minLoopCount
            // 计算出可放对数
            // 总数 / minLoopCount         类型一    双对存在便于配对
            let tile4Count = ~~(config.capacity / minLoopCount)
            // 总数 % minLoopCount / 2     类型二    单对存在无解增加
            let tile2Count = ~~((config.capacity % minLoopCount) / 2)
            console.log(tile4Count);
            for (let i = 0; i < tile4Count; i++) {
                let tile = tiles[i];
                console.log(tile);
                for (let j = 0; j < ~~(minLoopCount / tile.length); j++) {
                    tileList.push(...tile)
                }
            }
            for (let i = 0; i < tile2Count; i++) {
                let tile = tiles[tile4Count + i];
                console.log(tile);
                for (let j = 0; j < ~~(2 / tile.length); j++) {
                    tileList.push(...tile)
                }
            }
            // 将数组顺序打乱 
            console.log(tileList);
            tileList = Randoms.getDisorganizeArray(tileList)
            // 加载致DOM层
            let tileDomList = document.querySelectorAll('.tile-item')
            console.log(tileDomList.length, tileList.length);
            for (let i = 0; i < toEven(tileDomList.length); i++) {
                const tileDom = tileDomList.item(i)
                console.log(tileList[i]);
                tileDom.setAttribute("id", tileList[i].id)
                tileDom.setAttribute('tile', tileList[i].tile)
                if (tileList[i].type) {
                    tileDom.setAttribute('type', tileList[i].type)
                }
                tileDom.setAttribute("style", `${tileDom.getAttribute('style')};background-image: url(${tileList[i].tile})`)
                let x = tileDom.getAttribute('x')
                let y = tileDom.getAttribute('y')
                mapData[`(${x},${y})`] = tileList[i].id
            }
            // 检查无用盒子
            document.querySelectorAll(`.tile-item:not([id])`).forEach((item) => item.remove())
            console.log(mapData);
        }
        // 砖块点击事件
        function tileClick(event) {
            // 避免相同元素被点击 2 次 
            if (firstDom != event.target) {
                if (!firstDom) firstDom = event.target
                else lastDom = event.target;
                event.target.classList.add('active')
                if (firstDom && lastDom) {
                    let allowLink = firstDom.getAttribute('id') == lastDom.getAttribute('id');
                    let type = firstDom.getAttribute('type')
                    // 如果是cp图片不能相同
                    if (type == 'cp') {
                        allowLink = allowLink && firstDom.getAttribute('tile') != lastDom.getAttribute('tile')
                    }
                    if (allowLink) {
                        let linkPath = getLinkPath(firstDom, lastDom)
                        if (linkPath) {
                            console.log('允许连接');
                            drawLinkPath(linkPath)
                            playSound("linkSuccess")
                            eliminate()
                            checkGameOver();

                        }
                        else {
                            console.log('啊偶');
                            playSound("linkError")
                        }
                    }
                    else {
                        console.log('啊偶');
                        playSound("linkError")
                    }
                    firstDom.classList.remove('active')
                    lastDom.classList.remove('active')
                    firstDom = null;
                    lastDom = null;
                }
            }

        }
        /** 检查游戏是否结束 
         */
        function checkGameOver() {
            if (Object.values(mapData).filter(a => a).length == 0) {
                console.log('游戏胜利');
                winDom.hidden = false;
            }
            // 检查死局
            if (!checkStalemate()) {
                rearrange();
            }
        }
        /** 检查是否是死局 */
        function checkStalemate() {
            let mapList = Object.keys(mapData).map(key => {
                return {
                    v2: key,
                    id: mapData[key]
                }
            })

            let someIdArrList = group(mapList, (item) => item.id)

            for (let i = 0; i < someIdArrList.length; i++) {
                if (contrast(someIdArrList[i], (prev, next) => getLinkPathByV2(...strToV2(prev.v2), ...strToV2(next.v2)))) {
                    return true
                }
            }

            return false;
        }
        /** 重新排列 */
        function rearrange() {
            let origin = Object.keys(mapData)
            let target = Randoms.getDisorganizeArray(origin)

            for (let i = 0; i < origin.length; i++) {
                const okey = origin[i];
                const tkey = target[i];

                const [ox, oy] = strToV2(okey);
                const [tx, ty] = strToV2(tkey);

                const odom = document.querySelector(`.tile-item[x='${ox}'][y='${oy}']`)
                const tdom = document.querySelector(`.tile-item[x='${tx}'][y='${ty}']`)

                const otile = odom.getAttribute('tile')
                const ttile = tdom.getAttribute('tile')

                const oid = odom.getAttribute('id')
                const tid = tdom.getAttribute('id')

                tdom.setAttribute('id', oid)
                odom.setAttribute('id', tid)

                tdom.setAttribute('tile', otile)
                odom.setAttribute('tile', ttile)

                mapData[tkey] = oid
                mapData[okey] = tid

                tdom.style.backgroundImage = `url(${otile})`
                odom.style.backgroundImage = `url(${ttile})`
            }

            if (!checkStalemate()) {
                rearrange()
            }
        }
        /** 消除逻辑
         */
        function eliminate() {
            let start = {
                x: firstDom.getAttribute('x'),
                y: firstDom.getAttribute('y'),
            }
            let end = {
                x: lastDom.getAttribute('x'),
                y: lastDom.getAttribute('y'),
            }
            firstDom.remove();
            lastDom.remove();
            delete mapData[`(${start.x},${start.y})`]
            delete mapData[`(${end.x},${end.y})`]
            setTimeout(() => {
                let ctx = drawLineDom.getContext('2d')
                ctx.clearRect(0, 0, config.canvasWidth, config.canvasHeight)
            }, 300);
        }
        /** 播放音效
         */
        function playSound(name) {
            /**
             * @type {HTMLAudioElement}
             */
            let audio = document.querySelector("#sound_audio")
            audio.setAttribute('src', config.sounds[name])
            audio.addEventListener("canplay", (ev) => {
                audio.play()
            })
        }
        /** 绘制连接路径线
         */
        function drawLinkPath(paths) {
            drawLineDom.width = config.canvasWidth
            drawLineDom.height = config.canvasHeight
            let ctx = drawLineDom.getContext('2d')
            ctx.clearRect(0, 0, config.canvasWidth, config.canvasHeight)
            ctx.beginPath()
            ctx.lineWidth = 2;
            for (let i = 0; i < paths.length; i++) {
                let { x, y } = paths[i]
                x = firstDom.offsetWidth / 2 + config.itemWidth * (x + 1)
                y = firstDom.offsetHeight / 2 + config.itemHeight * (y + 1)
                if (!i) ctx.moveTo(x, y)
                else ctx.lineTo(x, y)
            }
            ctx.stroke();
        }
        /** 获取可连接的路径
         * @param { HTMLDivElement } t1  
         * @param { HTMLDivElement } t2  
         */
        function getLinkPath(t1, t2) {
            let x1 = +t1.getAttribute('x')
            let y1 = +t1.getAttribute('y')
            let x2 = +t2.getAttribute('x')
            let y2 = +t2.getAttribute('y')
            return getLinkPathByV2(x1, y1, x2, y2)
        }
        /** 获取可连接的路径
         * @param { number } x1  
         * @param { number } y1  
         * @param { number } x2  
         * @param { number } y2  
         */
        function getLinkPathByV2(x1, y1, x2, y2) {
            let path = []
            let minX = Math.min(x1, x2)
            let minY = Math.min(y1, y2)
            let maxX = Math.max(x1, x2)
            let maxY = Math.max(y1, y2)


            // 相邻 
            if (y1 == y2 && Math.abs(x1 - x2) == 1 || x1 == x2 && Math.abs(y1 - y2) == 1) {
                return [{ x: x1, y: y1 }, { x: x2, y: y2 }]
            }
            // L0 直连 只用判断一次 只会存在一种
            // 横向 纵向
            if (y1 == y2 || x1 == x2) {
                console.log('检查L0');
                let path = checkLinkPath(generateStraightLinePath({ x: x1, y: y1 }, { x: x2, y: y2 }))
                if (path) return path
            }
            // L1
            // 两种直角弯
            if (x1 != x2 && y1 != y2) {
                console.log('检查L1');
                let path = checkLinkPath(
                    generateStraightLinePath({ x: x1, y: y1 }, { x: x2, y: y1 }, { x: x2, y: y2 }),
                    generateStraightLinePath({ x: x1, y: y1 }, { x: x1, y: y2 }, { x: x2, y: y2 })
                )
                if (path) return path
            }

            // L2 
            // 内二折 x轴
            if (Math.abs(x1 - x2) > 1) {
                console.log('内二折 x轴');
                if (x1 < x2) {
                    for (let x = x1 + 1; x < x2; x++) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x1, y: y1 }, { x, y: y1 }, { x, y: y2 }, { x: x2, y: y2 }))
                        if (path) return path
                    }
                }
                else {
                    for (let x = x2 + 1; x < x1; x++) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x2, y: y2 }, { x, y: y2 }, { x, y: y1 }, { x: x1, y: y1 }))
                        if (path) return path
                    }
                }
            }
            if (Math.abs(y1 - y2) > 1) {
                console.log('内二折 y轴');
                if (y1 < y2) {
                    for (let y = y1 + 1; y < y2; y++) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x1, y: y1 }, { x: x1, y }, { x: x2, y }, { x: x2, y: y2 }))
                        if (path) return path
                    }
                }
                else {
                    for (let y = y2 + 1; y < y1; y++) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x2, y: y2 }, { x: x2, y }, { x: x1, y }, { x: x1, y: y1 }))
                        if (path) return path
                    }
                }
            }

            if (y1 != y2) {
                console.log('外二折');
                if (x1 < x2) {
                    console.log('外二折 x1<x2');
                    for (let x = x1 - 1; x >= -1; x--) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x1, y: y1 }, { x, y: y1 }, { x, y: y2 }, { x: x2, y: y2 }))
                        if (path) return path
                    }
                    for (let x = x2 + 1; x <= config.rows; x++) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x2, y: y2 }, { x, y: y2 }, { x, y: y1 }, { x: x1, y: y1 }))
                        if (path) return path
                    }
                }
                else {
                    console.log('外二折 x1>x2');
                    for (let x = x2 - 1; x >= -1; x--) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x2, y: y2 }, { x, y: y2 }, { x, y: y1 }, { x: x1, y: y1 }))
                        if (path) return path
                    }
                    console.log("外二折 x = x1 + 1");
                    for (let x = x1 + 1; x <= config.rows; x++) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x1, y: y1 }, { x, y: y1 }, { x, y: y2 }, { x: x2, y: y2 }))
                        if (path) return path
                    }
                }
            }

            if (x1 != x2) {
                console.log('外二折');
                if (y1 < y2) {
                    for (let y = y1 - 1; y >= -1; y--) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x1, y: y1 }, { x: x1, y }, { x: x2, y }, { x: x2, y: y2 }))
                        if (path) return path
                    }
                    for (let y = y2 + 1; y <= config.cols; y++) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x2, y: y2 }, { x: x2, y }, { x: x1, y }, { x: x1, y: y1 }))
                        if (path) return path
                    }
                }
                else {
                    for (let y = y2 - 1; y >= -1; y--) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x2, y: y2 }, { x: x2, y }, { x: x1, y }, { x: x1, y: y1 }))
                        if (path) return path
                    }
                    for (let y = y1 + 1; y <= config.cols; y++) {
                        let path = checkLinkPath(generateStraightLinePath({ x: x1, y: y1 }, { x: x1, y }, { x: x2, y }, { x: x2, y: y2 }))
                        if (path) return path
                    }
                }
            }
        }
        /** 检查连接路径
         *  忽略起点和目标点
         */
        function checkLinkPath(...paths) {
            for (let i = 0; i < paths.length; i++) {
                let path = paths[i];
                let middle = []
                for (let j = 1; j < path.length - 1; j++) {
                    let prev = path[j - 1]
                    let { x, y } = path[j];
                    // 相连性
                    if (mapData[`(${x},${y})`] || Math.abs(prev.x - x) > 1 || Math.abs(prev.y - y) > 1) {
                        middle = []
                        break;
                    }
                    else middle.push({ x, y })
                }
                if (middle.length > 0) {
                    return path
                }
            }
        }
        /** 生成范围数字 例如 1 - 7) [1,2,3,4,5,6] or [7,6,5,4,3,2]
         * @param {number} start
         * @param {number} end
         * @param {number} step
         */
        function generateRangeNumber(start, end, step = 1) {
            let nums = []
            if (end > start) {
                for (let i = start; i < end; i += step) {
                    nums.push(i)
                }
            }
            else {
                for (let i = start; i > end; i -= step) {
                    nums.push(i)
                }
            }
            return nums;
        }
        /** 通过一些坐标点生成直线路径 [start,...,end]
         */
        function generateStraightLinePath(...points) {
            let path = []
            for (let i = 1; i < points.length; i++) {
                let start = points[i - 1]
                let end = points[i]
                if (start.x == end.x) {
                    path = path.concat(generateRangeNumber(start.y, end.y).map(n => Object({ x: start.x, y: n })))
                }
                if (start.y == end.y) {
                    path = path.concat(generateRangeNumber(start.x, end.x).map(n => Object({ x: n, y: start.y })))
                }
            }
            path.push(points[points.length - 1])
            return path
        }
        /** 通过某条件进行打组
         */
        function group(arr, func) {
            let tmpDict = {};
            for (let i = 0; i < arr.length; i++) {
                if (!tmpDict[func(arr[i], i)]) tmpDict[func(arr[i], i)] = []
                tmpDict[func(arr[i], i)].push(arr[i])
            }
            return Object.values(tmpDict)
        }
        /** 通过条件一一对比 */
        function contrast(arr, func) {
            for (let i = 0; i < arr.length - 1; i++) {
                for (let j = i + 1; j < arr.length; j++) {
                    if (func(arr[i], arr[j])) return true;
                }
            }
            return false;
        }
        /** 字符串变更为数字格式 */
        function strToV2(str) {
            console.log(str);
            let [x, y] = str.match(/[0-9.]{1,}/g);
            console.log(x, y);
            return [+x, +y]
        }
        /** 向下转为偶数 */
        function toEven(num) {
            return num % 2 == 0 ? num : num - 1
        }
        // 主程序执行
        function main() {
            loadConfig();
        }
        main(); 
    </script>
</body>

</html>